feat(agents): harness-agnostic effort write-side + spawn legacy bridge (Goose + Claude Code) - #4625
Draft
wpfleger96 wants to merge 5 commits into
Draft
feat(agents): harness-agnostic effort write-side + spawn legacy bridge (Goose + Claude Code)#4625wpfleger96 wants to merge 5 commits into
wpfleger96 wants to merge 5 commits into
Conversation
wpfleger96
force-pushed
the
duncan/effort-write-genericize
branch
from
August 4, 2026 01:36
2d8f97a to
e9e8f6a
Compare
wpfleger96
force-pushed
the
duncan/effort-write-genericize
branch
6 times, most recently
from
August 4, 2026 17:35
bb1b659 to
c93e1c4
Compare
…entity + alias normalization (Phase 1) - EffortNormalization struct in runtime_metadata.rs with canonical values, aliases, and normalize_str(); GOOSE_EFFORT_NORMALIZATION static cited to goose thinking.rs:277-308 (canonical: off/low/medium/high/max; aliases: none|disabled→off, med→medium, xhigh→max) - effort_normalization field on KnownAcpRuntime; accepted_effort_values on AcpRuntimeCatalogEntry (serialized to TS as the single canonical authority) - LEGACY_THINKING_EFFORT_KEY and ALL_KNOWN_EFFORT_KEYS constants in config_bridge/mod.rs; effort_tier_alias() for per-tier native+legacy key resolution with normalization and global-tier exclusion - find_effort_config_option() identifies live effort by ACP category thought_level (Goose/claude-agent-acp); falls back to legacy effort category for transition compatibility; thought_level always wins when both present; returns (value, config_id) so write-route uses the real entry id - build_thinking_field() normalizes all candidates before precedence, override tracking, and B-equality; B-collapse compares normalized ACP value against normalized non-ACP winner — none↔off and xhigh↔max collapse correctly; write_via uses actual matched config_id (not hardcoded effort) - Test hygiene: test_runtime()/buzz_agent_runtime() use catalog lookups; 6 goose_runtime() tests wrapped with with_no_goose_config(); 4 readiness stubs gain effort_normalization: None - 6 new tests in reader_tests_ext.rs pin real Goose ACP shape, both-present priority, legacy fallback, alias B-collapse (none↔off, xhigh↔max), and env alias normalization Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…tation contract (Phase 2) - acceptedEffortValues on AcpRuntimeCatalogEntry: Goose carries canonical off|low|medium|high|max (source: goose thinking.rs:277-308); null for buzz-agent (per-model catalog). Single authority for UI, spawn bridge, and reader. - normalizeEffortValue() in buzzAgentConfig.ts: aliases none|disabled→off, med→medium, xhigh→max (case-insensitive); null acceptedValues = pass-through (buzz-agent path unchanged). - ALL_KNOWN_EFFORT_KEYS in buzzAgentConfig.ts mirrors Rust constant; used for record/persona runtime-transition cleanup. - agentConfigCore.ts: native key wins; legacy-only pre-migration save shown via effortValue fallback; legacyConsumedKey suppresses duplicate advanced row. optionSource harnessNative when acceptedEffortValues present. - AgentConfigFields.tsx: descriptor-driven global/onboarding path for harnessNative runtimes (isHarnessNativeEffort): canonical options from acceptedEffortValues, currentEffort from normalised descriptor value, effortValidForAutoClear from acceptedEffortValues. Unconditional BUZZ_AGENT_THINKING_EFFORT deletes guarded by !isHarnessNativeEffort (Delta 5: global-scope coexistence). allStructuredKeys includes legacyConsumedKey; effort clear deletes native + legacy; bakedStructuredKeys() replaces static set. - buzzAgentModelTuningFields.tsx: applyHarnessNativeEffortChange() exported production helper — single spec for the mutation contract. legacyKey=null at global/onboarding scope (Delta 5), legacyKey=BUZZ_AGENT_THINKING_EFFORT at record/persona scope. EffortSelectField gains canonicalValues prop to bypass Buzz 7-value catalog. HarnessNativeEffortFields calls production helper. - personaRuntimeModel.ts: resolveInheritedRuntimeSubmission gains excludePersonaEnvKeys param. Strips harness-native effort keys from persona layer on inherit-transition so the record never acquires a frozen copy of persona effort (effort inherits at spawn; it is not a credential). - AgentInstanceEditDialog.tsx: passes excludePersonaEnvKeys with runtime's thinkingEnvVar + ALL_KNOWN_EFFORT_KEYS when runtime has acceptedEffortValues. - bakedEnvHelpers.ts: getInheritedAgentDefaults accepts nativeEffortKey option; baked lookup is native-only for non-buzz-agent runtimes (Delta-4 boundary). bakedStructuredKeys() exported for AgentConfigFields. - useAgentDialogDefaults.ts: accepts nativeEffortKey; seeds inherited env under native key. - AgentDefinitionDialog + AgentInstanceEditDialog: pass prev/next native effort keys to selectionOnRuntimeChange; handleInheritHarnessChange clears all known effort keys on pin↔inherit toggle. - runtimeModelProviderSelection.ts: selectionOnRuntimeChange clears prev native + next native + legacy at record/persona scope; skip when native === legacy (buzz-agent same-key invariant). - agentConfigOptions.tsx: resetConfigForHarnessChange preserves all effort keys (global-scope Delta-5 rule; each runtime's descriptor strips foreign keys). - agent_config.rs: is_safe_to_reveal derives allowed effort keys from ALL_KNOWN_EFFORT_KEYS so GOOSE_THINKING_EFFORT is unmasked at baked tier. - AGENTS.md rule-2 'until PR 2.7' clause removed: native-key persistence lands in this commit. - Test coverage: applyHarnessNativeEffortChange record/persona + global scope pins; resolveInheritedRuntimeSubmission inherit-no-materialize (native + legacy), concrete-override-wins, non-effort-merged, full-submit-payload-clean; global Goose descriptor optionSource/value/invalid/no-legacy pins; runtimeModelProviderSelection useLiteralKeys cleanup. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…st (Phase 3) - apply_effort_bridge() in config_bridge/mod.rs: applied after merged user env in resolve_effective_agent_env_with_def. Strips all foreign known effort keys (runtime-scoped invariant), then resolves canonical effort via tier-first precedence (record native → record legacy → persona native → persona legacy → global native → definition native; global legacy excluded end-to-end). Removes raw native key (possibly alias-form) from env before inserting canonical winner — skip-as-absent applies to the output map, not just winner selection. - effort_tier_alias() with global_tier=true: honours native key only at global tier; legacy alias excluded there (plan v3 Delta 2). - readiness_effort_bridge_tests.rs: 12 spawn tests covering bridge activation, tier-first precedence (record beats persona, record legacy beats persona native, global legacy excluded), alias normalization (none→off, xhigh→max), invalid-skip-as-absent (minimal skipped, persona native wins), and the bidirectional global coexist invariant (Goose and buzz-agent from same global config each receive only their own native key; GOOSE_THINKING_EFFORT stripped from buzz-agent descriptor, BUZZ_AGENT_THINKING_EFFORT stripped from Goose). - agents_deploy.rs: deploy parity test pins that a legacy-only Goose record produces GOOSE_THINKING_EFFORT in launch.env while top-level env_vars retains the legacy key as compatibility bookkeeping (Delta-4 contract boundary). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… one metadata contract
Thufir's review identified three IMPORTANT defects (all live display/spawn
divergences — the same class LP-EFFORT-VISIBILITY targets):
1. Baked native effort deleted at spawn while the UI reported it as inherited.
Rust: apply_effort_bridge now carries baked_env as the lowest-precedence tier
(native-key only, no legacy alias). TS: resolveBakedEffort extracted as a
shared helper in bakedEnvHelpers.ts; AgentConfigFields (global/onboarding)
now routes through it so baked xhigh→max and invalid minimal→absent on both
display and spawn. Pins: resolveBakedEffort_{xhigh_normalizes_to_max,
invalid_minimal_returns_null}.
2. Definition-tier alias-policy inversion: spawn passed allow_legacy_alias=false
correctly at global but incorrectly at definition. The bool param is renamed
to allow_legacy_alias with an explicit doc table; global and definition cannot
silently diverge again.
3. Metadata contract duplicated across Rust and TS: EffortNormalization.aliases
serialized through AcpRuntimeCatalogEntry.effort_aliases; normalizeEffortValue
fallback alias table removed — absent effortAliases means no aliases (not
Goose aliases). effortAliases threaded through all production call sites
(AgentDefinitionDialog, AgentInstanceEditDialog, HarnessNativeEffortFields).
useAgentDialogDefaults gains a runtime param so callers pass one object
instead of three; AgentDefinitionDialog/AgentInstanceEditDialog use it.
ALL_KNOWN_EFFORT_KEYS static constant removed; allKnownEffortKeys() derives
from runtime declarations in both Rust and TS. Fixtures in agentConfigCore
and buzzAgentConfig tests updated to carry effortAliases; non-Goose
ultra→max pin proves normalization comes from the descriptor, not a table.
Rust ALL_KNOWN_EFFORT_KEYS → all_known_effort_keys() (derived from
KNOWN_ACP_RUNTIMES); apply_effort_bridge: #[allow(clippy::too_many_arguments)]
added for the 8th param.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
force-pushed
the
duncan/effort-write-genericize
branch
from
August 4, 2026 18:32
c93e1c4 to
febf701
Compare
Claude Code exposes CLAUDE_CODE_EFFORT_LEVEL (low|medium|high|xhigh|max) as a first-class env var that the claude-agent-acp adapter inherits from its parent process and passes through to the SDK subprocess env. Adding Claude to the catalog contract is therefore a pure catalog declaration — no new UI logic, no per-harness special casing. Changes: - Add CLAUDE_EFFORT_NORMALIZATION static with 5-value vocabulary and no aliases (Claude accepts values natively; xhigh is canonical, not an alias as it is for Goose). - Wire thinking_env_var and effort_normalization into the claude entry in KNOWN_ACP_RUNTIMES; import CLAUDE_EFFORT_NORMALIZATION in discovery.rs. - Remove the Claude deferral branch (render: deferredUntilNativeOptions- Available) from agentConfigCore.ts — Claude now hits the existing harnessNative path just like Goose. - Update runtimeModelProviderSelection.ts comment: buzz→claude now fires the else-if cleanup branch; claude→goose fires the if branch. - Update AGENTS.md rule 3: Claude effort is no longer deferred. - Rust pins: catalog vocabulary/no-alias contract, spawn native survival, invalid-value skip, foreign-key stripping, legacy alias at record tier (valid and invalid-for-Claude cases). - TS pins: valid values pass through, Goose aliases absent from Claude vocab, empty alias array means no fallback table. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
force-pushed
the
duncan/effort-write-genericize
branch
from
August 4, 2026 21:58
80df98f to
7c4c183
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Genericizes the agent effort write-side so non-Buzz runtimes (Goose and Claude Code) participate in the same canonical effort contract as buzz-agent, with a spawn bridge that translates the canonical key to whatever the target harness expects at launch time.
Five commits on top of
main(cb4a73e17).Phase 1 — B-collapse + ACP identity + alias normalization
desktop/src-tauri/src/managed_agents/config_bridge/thought_leveloption whose raw value equalled the global default was reported as a live override instead of collapsing toGlobalDefault. Fixed: compare via normalized values after applyingeffort_norm.thought_levelACP identity. When boththought_levelandthinking_effortACP categories are present,thought_levelwins. The winning entry'sconfig_idis captured and forwarded towrite_via; it is always entry-supplied, never hardcoded.effort_normbefore candidate-list construction and B comparison. An invalid ACP value (e.g.minimalfor Goose) is omitted as a value candidate so a lower tier wins;write_viastaysAcpSetConfigOptionwith the entry-suppliedconfig_idbecause the route targets the option, not its current value.normalizeEffortValueinbuzzAgentConfig.tsuses the runtime-suppliedeffortAliasesdescriptor as the single normalization authority. AbsenteffortAliasesmeans no aliases — the built-in Goose fallback table has been removed. Canonical values pass through; unrecognised values returnnull.Phase 2 — Canonical effort contract + native-key persistence + mutation contract
desktop/src/features/agents/Single read policy.
resolveEffortFromEnv(envVars, nativeKey, legacyKey, acceptedValues, effortAliases)inagentConfigCore.tsis the authoritative read path for all effort surfaces. It normalizes, applies native-first / valid-legacy-fallback, and returns{ value, legacyConsumed }.deriveAgentConfigFieldModel,HarnessNativeEffortFields, and both editors'legacyEffortConsumedmemos all call it.Descriptor.
deriveAgentConfigFieldModelemits an effort field with:optionSource: "harnessNative"for runtimes with a staticacceptedEffortValuesvocabulary (Goose, Claude Code);"buzzAgentCatalog"for buzz-agent;"legacyProviderModelCatalog"otherwise.acceptedEffortValuesfrom runtime metadata — never the seven-value buzz-agent list.legacyKey = BUZZ_AGENT_THINKING_EFFORT) at record/persona scope only; omitted at global/onboarding (Delta 5: that key is buzz-agent's own native key there).UI surfaces.
AgentConfigFields(global/onboarding),PersonaAdvancedFields(persona/definition), andEditAgentAdvancedFields(record/instance) all branch onoptionSource === "harnessNative". For harness-native runtimes they renderHarnessNativeEffortFieldswith runtime metadata;EffortSelectFieldreceives canonical choices fromacceptedEffortValues, not the Buzz catalog.HarnessNativeEffortFields. AcceptslegacyEnvKey: string | null(required —nullat global/onboarding enforces the tier boundary and prevents silent legacy key deletion). Reads viaresolveEffortFromEnv. Save = canonical native write + atomic legacy delete at record/persona scope; native-key-only at global scope (preserves coexisting foreign native keys — Delta 5).applyHarnessNativeEffortChange. Exported production mutation helper called by both the component andAgentConfigFields's onChange. Encodes the scope-split policy:legacyKey = BUZZ_AGENT_THINKING_EFFORTat record/persona;legacyKey = nullat global. Tests import and exercise it directly.Model-coupled mutation guard. Heal-on-mount, orphan-clear, and
useEffortAutoClearinAgentConfigFieldsskip the native effort key whenisHarnessNativeEffort. Goose and Claude Code effort is model-independent — their vocabularies are static; the reader's skip-as-absent contract handles invalid values non-destructively. TheuseEffortAutoClearhook receives""ascurrentEffortfor harness-native runtimes, making it a no-op.Inherit-transition strip.
resolveInheritedRuntimeSubmissionacceptsexcludePersonaEnvKeysto strip effort keys from the persona merge layer on a pinned→inherit transition.AgentInstanceEditDialogpasses[thinkingEnvVar, ...allKnownEffortKeys(runtimes)]when the runtime hasthinkingEnvVar— derives the known-key set from the catalog, not a static constant.Legacy row visibility.
legacyConsumedKeycontrols advanced-row suppression. The legacy row is hidden only when the key was validly consumed; invalid or unrecognised legacy values remain visible as advanced env rows.Phase 3 — Shared alias resolver + spawn effort bridge + deploy test
desktop/src-tauri/effort_tier_alias.managed_agents/config_bridge/mod.rsexposeseffort_tier_alias(value, accepted_values, allow_legacy_alias)— the shared normalizer used bybuild_thinking_fieldandapply_effort_bridge. The boolean parameter isallow_legacy_aliaswith an explicit per-tier doc table (record: true, persona: true, global: false, definition: false, baked: false) so no tier can silently diverge.apply_effort_bridge.managed_agents/config_bridge/mod.rsbridges canonical effort onto the spawn payload:all_known_effort_keys()(derived fromKNOWN_ACP_RUNTIMES::thinking_env_var, not a static constant).xhighnormalizes tomax; invalid baked values skip as absent.allow_legacy_alias=false— mirrors the reader's contract (plan v3 Delta 2).launch_config.launch.envfor deploy/cloud agents; top-levelenv_varsis left unbridged.One metadata contract.
EffortNormalization.aliasesis serialized throughAcpRuntimeCatalogEntry.effort_aliasesindiscovery.rsand surfaced throughRawAcpRuntimeCatalogEntry/AcpRuntimeCatalogEntryin TS.normalizeEffortValueuses the descriptor-supplied alias table; absenteffortAliasesmeans no aliases (not Goose aliases). All production call sites pass the descriptor:AgentConfigFieldsviaresolveBakedEffort, both dialogs viauseAgentDialogDefaults(runtime: ...),HarnessNativeEffortFieldsvianormalizeEffortValue(raw, acceptedEffortValues, effortAliases). The staticALL_KNOWN_EFFORT_KEYSconstant (Rust and TS) is replaced byall_known_effort_keys()/allKnownEffortKeys(runtimes)derived from runtime declarations — adding a runtime automatically participates in foreign-key stripping without touching a constant.Baked-effort display.
resolveBakedEffort(bakedEnv, nativeKey, runtime)inbakedEnvHelpers.tsis the shared resolver for every baked-effort display surface:AgentConfigFields(global/onboarding) andgetInheritedAgentDefaults. Both normalize through alias→canonical / invalid→absent so display parity with spawn is maintained.useAgentDialogDefaultsgains aruntimeparam; callers passruntime: runtimes.find(...)orruntime: prospectiveRuntimein one line rather than three separate props.Phase 4 — Claude Code effort enablement
desktop/src-tauri/src/managed_agents/discovery/+desktop/src/features/agents/lib/Catalog declaration.
CLAUDE_EFFORT_NORMALIZATIONadded inruntime_metadata.rswith canonical vocabularylow | medium | high | xhigh | maxand no aliases (Claude Code accepts these natively;xhighis canonical, not an alias as it is for Goose). Theautovalue is deliberately excluded — unset env already means model-default. Available levels are per-model, but Claude Code falls back to the nearest supported level, so a static vocabulary declaration is safe.Catalog wiring. The
claudeentry inKNOWN_ACP_RUNTIMESnow declaresthinking_env_var: Some("CLAUDE_CODE_EFFORT_LEVEL")andeffort_normalization: Some(&CLAUDE_EFFORT_NORMALIZATION). The env var is inherited by theclaude-agent-acpNode.js adapter process from Buzz's spawn env, which passes it throughprocess.envto the underlying SDK subprocess env and then to theclaudeCLI binary.UI policy removal. The
else if (runtime?.id === "claude")deferral branch (render: "deferredUntilNativeOptionsAvailable") inagentConfigCore.tsis removed. Claude now hits the existingif (runtime?.thinkingEnvVar)path and emits aharnessNativedescriptor withrender: "control"— zero new UI code.Test coverage
thought_levelpriority, ACP alias/case/invalid normalization, alias resolver tiers, bridge descriptor hygiene, deploy parity boundary; baked tier (high/xhigh→max/minimal→absent/legacy-excluded/beaten-by-record); definition tier (legacy excluded, native accepted) in both spawn and reader; Claude Code catalog vocabulary/no-alias pins, spawn native survival, invalid-value skip, foreign-key stripping, legacy alias at record tier (valid and invalid-for-Claude cases).resolveEffortFromEnvread policy (native-first, legacy fallback, tier boundary, null-legacyKey);applyHarnessNativeEffortChangescope-split mutation;deriveAgentConfigFieldModelat global/definition/instance scope;HarnessNativeEffortFieldslegacy migration;excludePersonaEnvKeysinherit-transition strip;isHarnessNativeEffortguard (xhigh survives model-heal; minimal survives; buzz-agent auto-clear unchanged); inherited-defaults normalization (global xhigh→max, baked xhigh→max, baked minimal→absent); global coexisting native-pair preservation;resolveBakedEffortdirect pins (xhigh→max, minimal→null — the production helper used by global/onboarding);normalizeEffortValueabsent-aliases pin and non-Goose ultra→max pin (TS normalization comes from supplied descriptor, not any runtime-specific table); Claude Code harnessNative descriptor shape, valid values pass-through, Goose aliases absent from Claude vocab.Deferred
useAgentDialogDefaultsretains three unused optional inputs (nativeEffortKey,acceptedEffortValues,effortAliases) superseded by theruntimeparam. Follow-up cleanup: remove them and requireruntime.